package org.jpropelleralt.list.impl;

import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.change.ChangeDefault;
import org.jpropelleralt.collection.CollectionDeltaType;
import org.jpropelleralt.list.ListBox;
import org.jpropelleralt.list.ListChange;
import org.jpropelleralt.list.ListDelta;
import org.jpropelleralt.reaction.Reaction;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.ref.impl.RefDefault;
import org.jpropelleralt.set.SetBox;
import org.jpropelleralt.set.impl.SetBoxDefault;

/**
 * Allows for creation of a {@link Ref} that
 * tracks multiple selections in a {@link ListBox}
 */
public class ListSelectionIndices {

	/**
	 * Create a {@link Ref} that will maintain a set of selection
	 * indices in the {@link ListBox} value of a {@link Ref},
	 * where possible.
	 * @param <T>		The type of element in the {@link ListBox} 
	 * @param listRef	The {@link Ref} to a {@link ListBox}
	 * @param selectFirstByDefault	If true, the selection will be set to just the first
	 * 								element when it is reset. Selection is reset when
	 * 								the list is completely changed, or the selection is deleted,
	 * 								or the selection is otherwise cleared.
	 * 
	 * @return			A list selection index {@link Ref}
	 */
	public static <T> Ref<SetBox<Integer>> create(Ref<ListBox<T>> listRef, boolean selectFirstByDefault) {
		
		SetBox<Integer> setBox = SetBoxDefault.create(listRef.features().universe());
		
		//This Ref must NOT be writeable - therefore it will retain the same
		//set contents. The set itself is kept up to date by the reaction. Hence the
		//ref contents are always correct. If we let someone put a new SetBox in the ref,
		//it would interfere with this.
		Ref<SetBox<Integer>> ref = RefDefault.create(listRef.features().universe(), null, setBox, false);
		
		ListSelectionReaction<T> reaction = new ListSelectionReaction<T>(listRef, setBox, selectFirstByDefault);

		//Keep reaction in scope while setbox is in scope
		//Note that if Ref is kept in scope, this keeps setbox and reaction in scope as well
		setBox.features().registerReaction(reaction);
		
		//We react to listRef and setBox - we don't need to listen to ref, since it just contains setBox, forever 
		listRef.features().addReaction(reaction);
		setBox.features().addReaction(reaction);
		
		//Start out with reaction up to date and with cached listBox
		reaction.apply(true);
		
		return ref;
	}
	
	private static class ListSelectionReaction<T> implements Reaction {

		private final Ref<ListBox<T>> listRef;
		private ListBox<T> listBox = null;
		private final SetBox<Integer> selection;
		private final boolean selectFirstByDefault;
		
		Map<Box, Change> shallowChange = new IdentityHashMap<Box, Change>();
		
		private Action action = Action.NOTHING;
		private final List<ListChange> changesForAdjustment = new LinkedList<ListChange>();
		private boolean newUserSelection = false;

		/**
		 * Action to carry out when applied
		 */
		private enum Action {
			RESET,
			ADJUST,
			NOTHING
		}
		
		private ListSelectionReaction(Ref<ListBox<T>> listRef, SetBox<Integer> selection, boolean selectFirstByDefault) {
			this.listRef = listRef;
			this.selection = selection;
			this.selectFirstByDefault = selectFirstByDefault;
			
			//When we apply, we change the selection
			shallowChange.put(selection, ChangeDefault.shallowInstance());
		}
		
		@Override
		public void apply(boolean propagate) {
			
			//Find current indices, and work out new indices
			Set<Integer> indices = new HashSet<Integer>(selection);
			Set<Integer> newIndices = new HashSet<Integer>(indices);

			//Reset
			if (action == Action.RESET) {
				newIndices.clear();
				
			//Adjust index
			} else if (action == Action.ADJUST) {
				//Check we still have correct cached listBox
				if (listBox != listRef.get()) {
					throw new RuntimeException("Invalid cached listBox in ListSelectionIndex");
				}
				adjustIndices(newIndices);
			}

			//Clear action and changes for next time
			action = Action.NOTHING;
			changesForAdjustment.clear();

			//Make sure we have the current listBox stored for later
			listBox = listRef.get();
			
			//Filter any invalid indices from new set
			Iterator<Integer> it = newIndices.iterator();
			while (it.hasNext()) {
				Integer newIndex = (Integer) it.next();
				if ((newIndex < -1) || (newIndex >= listBox.size())) {
					it.remove();
				}
			}

			//Handle selecting index 0 by default, if required
			if (selectFirstByDefault && newIndices.isEmpty() && !listBox.isEmpty()) {
				newIndices.add(0);
			}

			//If we have an actual change, make it
			if (!indices.equals(newIndices)) {
				selection.reactionReplace(newIndices, propagate);
			}
		}

		private void adjustIndices(Set<Integer> indices) {
			
			//Apply effect of each delta
			for (ListChange change : changesForAdjustment) {
				for (ListDelta delta : change) {
					
					//Preserve selection even when altered
					if (delta.getType() == CollectionDeltaType.ALTERATION) {
						
					//Clear and complete changes just clear indices, no need to
					//process further deltas
					} else if (delta.getType() == CollectionDeltaType.CLEAR) {
						indices.clear();
					} else if (delta.getType() == CollectionDeltaType.COMPLETE) {
						indices.clear();
						
					//Respond to deletion
					} else if (delta.getType() == CollectionDeltaType.DELETION) {
						
						//Build a set of indices to add back into selection, while 
						//we check through current indices, removing any that change or are removed
						Set<Integer> indicesToAdd = new HashSet<Integer>();
						Iterator<Integer> it = indices.iterator();
						while (it.hasNext()) {
							Integer index = (Integer) it.next();
							
							//No change at all if we are before the first changed index
							if (index < delta.getFirstChangedIndex()) {
								//No alteration
								
							//Work out whether we are AFTER the removed range - e.g. if first removed is
							//2, and change size is -3, we removed 2,3,4 and so everything from (2-(-3) = 5) inclusive
							//is still present. We just need to shift selection back to allow for removed indices
							} else if (index >= delta.getFirstChangedIndex() - delta.getChangeSize()) {
								//Note "+", since the change size should be negative for a deletion, and we move index back
								int newIndex = index + delta.getChangeSize();
								it.remove();
								indicesToAdd.add(newIndex);
								
							//If we are in the removed range, just remove the index
							} else {
								it.remove();
							}							
						}
						
						//Make sure to add in the new indices (shifted from removed indices)
						indices.addAll(indicesToAdd);

					} else if (delta.getType() == CollectionDeltaType.INSERTION) {
						//Build a set of indices to add back into selection, while 
						//we check through current indices, removing any that change or are removed
						Set<Integer> indicesToAdd = new HashSet<Integer>();
						Iterator<Integer> it = indices.iterator();
						while (it.hasNext()) {
							Integer index = (Integer) it.next();
	
							if (index < delta.getFirstChangedIndex()) {
								//No alteration
							} else {
								int newIndex = index + delta.getChangeSize();
								it.remove();
								indicesToAdd.add(newIndex);
							}
						}
						
						//Make sure to add in the new indices (shifted from removed indices)
						indices.addAll(indicesToAdd);
					}
				}
			}
		}

		@Override
		public Map<Box, Change> react(Map<Box, Change> changes) {
			//If we see a change to the selection but NOT to the
			//listRef, then we have a genuine new selection, therefore 
			//we should do nothing and apply immediately
			if (changes.containsKey(selection) && !changes.containsKey(listRef)) {
				newUserSelection = true;
			}

			//We just make a shallow change - any change to anything we listen
			//to will probably make us adjust the selection
			return shallowChange;
		}

		@Override
		public boolean completeChanges(Map<Box, Change> changes) {
			
			//If we know what the listbox is, and we are not planning to
			//just reset, then adjust for any list changes
			if (listBox!=null && action != Action.RESET) {
				Change change = changes.get(listBox);
				if (change instanceof ListChange) {
					ListChange listChange = (ListChange) change;
					changesForAdjustment.add(listChange);
					action = Action.ADJUST;
				}
			}
			
			//But more importantly...
			//If listRef now contains a different ListBox, we
			//should reset the selection, and we don't know the listBox any more
			Change changeToListRef = changes.get(listRef);
			if (changeToListRef != null && changeToListRef.shallow()) {
				listBox = null;
				action = Action.RESET;
				changesForAdjustment.clear();
			}

			//But more importantly...
			//If we have a new user selection, then we should just keep
			//it, by doing nothing, and requesting immediate application
			if (newUserSelection) {
				action = Action.NOTHING;
				changesForAdjustment.clear();
				newUserSelection = false;
				return true;
			}

			//Otherwise, we need to apply immediately if cached listBox is now invalid
			return (listBox == null);
		}
	}
}
